// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fs/vfs.h>
#include <fbl/alloc_checker.h>
#include <fbl/ref_ptr.h>
#include <fbl/unique_ptr.h>
#include <lib/memfs/cpp/vnode.h>

#include <utility>

#include "dnode.h"

namespace memfs {

// Create a new dnode and attach it to a vnode
std::unique_ptr<Dnode> Dnode::Create(fbl::StringPiece name, fbl::RefPtr<VnodeMemfs> vn) {
  if ((name.length() > kDnodeNameMax) || (name.length() < 1)) {
    return nullptr;
  }

  fbl::AllocChecker ac;
  fbl::unique_ptr<char[]> namebuffer(new (&ac) char[name.length() + 1]);
  if (!ac.check()) {
    return nullptr;
  }
  memcpy(namebuffer.get(), name.data(), name.length());
  namebuffer[name.length()] = '\0';
  auto dn = std::unique_ptr<Dnode>(
      new Dnode(vn, std::move(namebuffer), static_cast<uint32_t>(name.length())));
  return dn;
}

std::unique_ptr<Dnode> Dnode::RemoveFromParent() {
  ZX_DEBUG_ASSERT(vnode_ != nullptr);

  std::unique_ptr<Dnode> node;
  // Detach from parent
  if (parent_) {
    node = parent_->children_.erase(*this);
    if (IsDirectory()) {
      // '..' no longer references parent.
      parent_->vnode_->link_count_--;
    }
    parent_->vnode_->UpdateModified();
    parent_ = nullptr;
    vnode_->link_count_--;
  }
  return node;
}

void Dnode::Detach() {
  ZX_DEBUG_ASSERT(children_.is_empty());
  if (vnode_ == nullptr) {  // Dnode already detached.
    return;
  }

  auto self = RemoveFromParent();
  // Detach from vnode
  self->vnode_->dnode_ = nullptr;
  self->vnode_ = nullptr;
}

void Dnode::AddChild(Dnode* parent, std::unique_ptr<Dnode> child) {
  ZX_DEBUG_ASSERT(parent != nullptr);
  ZX_DEBUG_ASSERT(child != nullptr);
  ZX_DEBUG_ASSERT(child->parent_ == nullptr);  // Child shouldn't have a parent
  ZX_DEBUG_ASSERT(child.get() != parent);
  ZX_DEBUG_ASSERT(parent->IsDirectory());

  child->parent_ = parent;
  child->vnode_->link_count_++;
  if (child->IsDirectory()) {
    // Child has '..' pointing back at parent.
    parent->vnode_->link_count_++;
  }
  // Ensure that the ordering of tokens in the children list is absolute.
  if (parent->children_.is_empty()) {
    child->ordering_token_ = 2;  // '0' for '.', '1' for '..'
  } else {
    child->ordering_token_ = parent->children_.back().ordering_token_ + 1;
  }
  parent->children_.push_back(std::move(child));
  parent->vnode_->UpdateModified();
}

zx_status_t Dnode::Lookup(fbl::StringPiece name, Dnode** out) {
  auto dn = children_.find_if([&name](const Dnode& elem) -> bool { return elem.NameMatch(name); });
  if (dn == children_.end()) {
    return ZX_ERR_NOT_FOUND;
  }

  if (out != nullptr) {
    *out = &(*dn);
  }
  return ZX_OK;
}

fbl::RefPtr<VnodeMemfs> Dnode::AcquireVnode() const { return vnode_; }

zx_status_t Dnode::CanUnlink() const {
  if (!children_.is_empty()) {
    // Cannot unlink non-empty directory
    return ZX_ERR_NOT_EMPTY;
  } else if (vnode_->IsRemote()) {
    // Cannot unlink mount points
    return ZX_ERR_UNAVAILABLE;
  }
  return ZX_OK;
}

struct dircookie_t {
  size_t order;  // Minimum 'order' of the next dnode dirent to be read.
};

static_assert(sizeof(dircookie_t) <= sizeof(fs::vdircookie_t),
              "MemFS dircookie too large to fit in IO state");

// Read the canned "." and ".." entries that should
// appear at the beginning of a directory.
zx_status_t Dnode::ReaddirStart(fs::DirentFiller* df, void* cookie) {
  dircookie_t* c = static_cast<dircookie_t*>(cookie);
  zx_status_t r;

  if (c->order == 0) {
    // TODO(smklein): Return the real ino.
    uint64_t ino = fuchsia_io_INO_UNKNOWN;
    if ((r = df->Next(".", VTYPE_TO_DTYPE(V_TYPE_DIR), ino)) != ZX_OK) {
      return r;
    }
    c->order++;
  }
  return ZX_OK;
}

void Dnode::Readdir(fs::DirentFiller* df, void* cookie) const {
  dircookie_t* c = static_cast<dircookie_t*>(cookie);
  zx_status_t r = 0;

  if (c->order < 1) {
    if ((r = Dnode::ReaddirStart(df, cookie)) != ZX_OK) {
      return;
    }
  }

  for (const auto& dn : children_) {
    if (dn.ordering_token_ < c->order) {
      continue;
    }
    uint32_t vtype = dn.IsDirectory() ? V_TYPE_DIR : V_TYPE_FILE;
    if ((r = df->Next(fbl::StringPiece(dn.name_.get(), dn.NameLen()), VTYPE_TO_DTYPE(vtype),
                      dn.AcquireVnode()->ino())) != ZX_OK) {
      return;
    }
    c->order = dn.ordering_token_ + 1;
  }
}

// Answers the question: "Is dn a subdirectory of this?"
bool Dnode::IsSubdirectory(const Dnode* dn) const {
  if (IsDirectory() && dn->IsDirectory()) {
    // Iterate all the way up to root
    while (dn->parent_ != nullptr && dn->parent_ != dn) {
      if (vnode_ == dn->vnode_) {
        return true;
      }
      dn = dn->parent_;
    }
  }
  return false;
}

fbl::unique_ptr<char[]> Dnode::TakeName() { return std::move(name_); }

void Dnode::PutName(fbl::unique_ptr<char[]> name, size_t len) {
  flags_ = static_cast<uint32_t>((flags_ & ~kDnodeNameMax) | len);
  name_ = std::move(name);
}

bool Dnode::IsDirectory() const { return vnode_->IsDirectory(); }

Dnode::Dnode(fbl::RefPtr<VnodeMemfs> vn, fbl::unique_ptr<char[]> name, uint32_t flags)
    : vnode_(std::move(vn)),
      parent_(nullptr),
      ordering_token_(0),
      flags_(flags),
      name_(std::move(name)) {}

Dnode::~Dnode() = default;

size_t Dnode::NameLen() const { return flags_ & kDnodeNameMax; }

bool Dnode::NameMatch(fbl::StringPiece name) const {
  return name == fbl::StringPiece(name_.get(), NameLen());
}

}  // namespace memfs
